package com.fit.utils.dev.code.generation.utils;

import com.alibaba.druid.pool.DruidDataSource;
import com.fit.utils.dev.code.generation.model.TablePrimaryModel;
import com.fit.utils.dev.code.generation.model.TemplateModel;
import com.fit.utils.se.EmptyUtils;
import com.fit.utils.se.IOUtil;
import com.fit.utils.se.StringUtil;
import com.fit.utils.se.VelocityUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.VelocityContext;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * MyBatis通用的mapper及service生成器
 * 生成文件：(以t_sys_user表为列)
 * 		
 * 		生成文件时,如果遇到相同的文件,不会覆盖(除SysUserModel.java外)
 * 		实体：(生成的树结构可在vm这个map中配置)
 * 			entity.
 * 				dao.SysUserMapper.java
 * 				mapper.SysUserMapper.xml			
 * 				model.
 * 					basic.SysUserModel.java			(此文件会覆盖生成,不需要模板)
 * 					SysUser.java					
 * 		service：
 * 			service.
 * 				impl.SysUserServiceImpl.java
 * 				SysUserService.java
 * 		controller:
 * 			web.controoler.SysUserController.java
 * 
 * @author hcl
 *
 */
@Slf4j
public class MyBatisGeneratorUtil {
	
	
	/**
	 * 是否生成service
	 */
	public static boolean GENERATE_SERVICE = true;
	
	/**
	 * 是否生成controller
	 */
	public static boolean GENERATE_CONTROLLER = true;
	
	/**
	 * 生成的java文件是否表按模块分包
	 * 此项为true,表名默认遵循【类型_模块_表名】命名规程
	 */
	public static boolean PACKAGE_POINTS_MODULE = true;
	
	/**
	 * 基于项目下 包 前面的路径
	 */
	public static String BASIC_PATH = "src/main/java";
	
	
	/**
	 * 是否覆盖生成文件(所有的文件都覆盖生成)
	 * 不要改此值,保持默认(false)就好
	 */
	public static boolean IS_COVER_GENERATOR = false;
	
	/**
	 * 模板信息配置
	 */
	public static Map<String,TemplateModel> VM = new HashMap<String,TemplateModel>();
	
	static{
		/**
		 * 模板配置(根据vm 配置 文件名称、文件基于模块后的路径(不能为空)、模板路径)
		 */
		VM.put("mapper", new TemplateModel("${modelName}Mapper.java", "entity/dao/","/local/generator/mybatis/mapper/template/Mapper.vm"));
		VM.put("mapperxml", new TemplateModel("${modelName}Mapper.xml", "entity/mapper/", "/local/generator/mybatis/mapper/template/Mapper.xml.vm"));
		VM.put("model", new TemplateModel("${modelName}.java","entity/model/", "/local/generator/mybatis/mapper/template/Model.vm"));
		VM.put("basicModel", new TemplateModel(null,"entity/model/basic/", null));
		VM.put("service", new TemplateModel("${modelName}Service.java","service/", "/local/generator/mybatis/mapper/template/Service.vm"));
		VM.put("serviceImpl", new TemplateModel("${modelName}ServiceImpl.java", "service/impl/", "/local/generator/mybatis/mapper/template/ServiceImpl.vm"));
		VM.put("controller", new TemplateModel("${modelName}Controller.java", "web/controller/","/local/generator/mybatis/mapper/template/Controller.vm"));
		//配置文件路径
		VM.put("config", new TemplateModel(null,null, "/local/generator/mybatis/mapper/template/generatorConfig.vm"));
	}
	
	
	
	
	/**
	 * 按模块生成Model
	 * @param dataSource 阿里云连接池,建议使用
	 * @param packageName 基础的包名
	 * @param tablePrefix 表前缀,表名生成modelname时将忽略此前缀
	 * @param moduleNameGet 根据表名获取模块名的抽象类,传入null将使用默认的方式生成
	 * @param moduleNames 模块名称集合
	 */
	public static void generatorByModule(
			DruidDataSource dataSourcePool,
			String packageName,
			String tablePrefix,
			ModuleName moduleNameGet,
			String... moduleNames){
		if(EmptyUtils.isEmpty(moduleNameGet))
			moduleNameGet = new ModuleName(){};
		
		if(EmptyUtils.isNotEmpty(moduleNames)){
			//获取所有表主键信息
			Map<String, TablePrimaryModel> tablePrimarys = getPrimaryKey(dataSourcePool, getDbNameByUrl(dataSourcePool.getUrl()));
			try {
				String sql = "select table_name from information_schema.tables where table_schema=?";
				List<Map<String, Object>> list = BasicDao.getDao().findBySql(sql, new String[]{getDbNameByUrl(dataSourcePool.getUrl())}, dataSourcePool.getConnection());
				Map<String,List<String>> tableModuleMap = new HashMap<String,List<String>>();
				for(Map<String, Object> t : list){
					String tableName = t.get("table_name").toString();
					String moduleName = moduleNameGet.getModuleName(tableName);
					if(EmptyUtils.isNotEmpty(moduleName)){
						List<String> moduleTables = tableModuleMap.get(moduleName);
						if(EmptyUtils.isEmpty(moduleTables)){
							moduleTables = new ArrayList<String>();
							tableModuleMap.put(moduleName, moduleTables);
						}
						moduleTables.add(tableName);
					}
				}
				
				for(String moduleName : moduleNames){
					List<String> moduleTables = tableModuleMap.get(moduleName);
					if(EmptyUtils.isNotEmpty(moduleTables)){
						generatorByTable(dataSourcePool, packageName, moduleNameGet, tablePrimarys, tablePrefix, moduleTables.toArray(new String[moduleTables.size()]));
					}else{
						log.error("没有查询到:{} 模块的表！",moduleName);
					}
				}
			} catch (SQLException e) {
				log.error("查询所有的表异常!",e);
			}
		}else{
			log.error("请传入模块名称！");
		}
	}
	
	/**
	 * 生成所有表的model
	 * @param dataSourcePool 连接池
	 * @param packageName 基础的包名
	 * @param tablePrefix 表前缀,表名生成modelname时将忽略此前缀
	 * @param moduleNameGet 根据表名获取模块名的抽象类,传入null将使用默认的方式生成
	 */
	public static void generatorAllTable(
			DruidDataSource dataSourcePool,
			String packageName,
			String tablePrefix,
			ModuleName moduleNameGet){
		try {
			String sql = "select table_name from information_schema.tables where table_schema=?";
			List<Map<String, Object>> list = BasicDao.getDao().findBySql(sql, new String[]{getDbNameByUrl(dataSourcePool.getUrl())}, dataSourcePool.getConnection());
			List<String> tables = new ArrayList<String>();
			for(Map<String, Object> t : list){
				tables.add(t.get("table_name").toString());
			}
			//获取所有表主键信息
			Map<String, TablePrimaryModel> tablePrimarys = getPrimaryKey(dataSourcePool, getDbNameByUrl(dataSourcePool.getUrl()));
			generatorByTable(dataSourcePool, packageName, moduleNameGet, tablePrimarys, tablePrefix, tables.toArray(new String[tables.size()]));
		} catch (SQLException e) {
			log.error("查询所有的表异常!",e);
		}
	}
	
	
	/**
	 * 按表名称生成所有表的Model
	 * @param dataSourcePool 数据源连接池
	 * @param packageName 基础的包名
	 * @param tablePrefix 表前缀,表名生成modelname时将忽略此前缀
	 * @param moduleNameGet 根据表名获取模块名的抽象类,传入null将使用默认的方式生成
	 * @param tableNames 表名集合
	 */
	public static void generatorByTable(
			DruidDataSource dataSourcePool,
			String packageName,
			String tablePrefix,
			ModuleName moduleNameGet,
			String... tableNames){
		//获取所有表主键信息
		Map<String, TablePrimaryModel> tablePrimarys = getPrimaryKey(dataSourcePool, getDbNameByUrl(dataSourcePool.getUrl()));
		generatorByTable(dataSourcePool, packageName, moduleNameGet, tablePrimarys, tablePrefix, tableNames);
	}
	
	
	
	
	/**
	 * 按表名生成Model
	 * @param dataSourcePool 
	 * @param packageName
	 * @param moduleNameGet
	 * @param tablePrimarys
	 * @param tablePrefix
	 * @param tableNames
	 */
	private static void generatorByTable(
			DruidDataSource dataSourcePool,
			String packageName,
			ModuleName moduleNameGet,
			Map<String, TablePrimaryModel> tablePrimarys,
			String tablePrefix,
			String... tableNames){
		if(EmptyUtils.isEmpty(moduleNameGet))
			moduleNameGet = new ModuleName(){};
		
		for(String tableName : tableNames){
			log.info("Create {} Model: ",tableName);
			
			
			//模块名称
			String moduleName = "";
			//基础包.模块包
			String modulePackage = packageName;
			//model名称
			String modelName = null;
			
			TablePrimaryModel tablePrimaryModel = tablePrimarys.get(tableName);
			if(EmptyUtils.isEmpty(tablePrimaryModel)){
				log.error("没有找到表：{} 的主键信息,跳过此表!",tableName);
				continue;
			}
			//获取当前表的模块
			if(PACKAGE_POINTS_MODULE){
				moduleName = moduleNameGet.getModuleName(tableName);
				if(EmptyUtils.isNotEmpty(moduleName)){
					modulePackage += "."+ moduleName;
				}else{
					log.error("没有获取到表：{}的模块名称！",tableName);
					throw new RuntimeException("没有获取到表：" + tableName + "的模块名称！");
				}
			}
			if(EmptyUtils.isNotEmpty(tablePrefix) && tableName.startsWith(tablePrefix)){
				modelName = StringUtil.underlineToCamelhump(tableName.substring(tablePrefix.length()),false);
			}else{
				modelName = StringUtil.underlineToCamelhump(tableName,false);
			}
			VelocityContext context = getParameter(tableName, modelName,modulePackage ,tablePrimaryModel, dataSourcePool);
			
			log.info("========生成表 {} 的basic model 开始========",tableName);
			generateByConfig(VelocityUtil.generateTemplateInstanceToInput(context, VM.get("config").getVmPath()));
			log.info("========生成表 {} 的basic model  结束========\r\n",tableName);
			
			log.info("========生成表 {} 的model 开始========",tableName);
			createFileByTemplate(VM.get("model"), context, modulePackage, IS_COVER_GENERATOR);
			log.info("========生成表 {} 的model 结束========\r\n",tableName);
			
			log.info("========生成表 {} 的mapper xml映射文件 开始========",tableName);
			//生成mapper xml文件,将自动生成的xml文件获取resultMap标签中的值,再根据模板生成新的xml文件
			TemplateModel mapperXmlConfig = VM.get("mapperxml");
			String mapperDir = getBasicModulePath(modulePackage)+mapperXmlConfig.getModulePath();
			File xmlFile = new File(mapperDir+context.get("modelName")+"ModelMapper.xml");
			if(xmlFile.exists()){
				String xmlStr;
				try {
					xmlStr = IOUtil.toString(new FileInputStream(xmlFile), "utf-8");
					Integer indexS = xmlStr.indexOf(">",xmlStr.indexOf("<resultMap"));
					String resultMapElements = xmlStr.substring(indexS+1, xmlStr.indexOf("</resultMap>",indexS+1));
					
					context.put("resultMapElements", resultMapElements);
					String xmlFileName = VelocityUtil.generateTemplateStrInstanceToString(context, mapperXmlConfig.getFileName());
					String xmlFileNameDir = getBasicModulePath(modulePackage)+mapperXmlConfig.getModulePath();
					File file = new File(xmlFileNameDir+xmlFileName);
					if(!file.exists()){
						//不存在此文件,就创建新的文件
						createFileByTemplate(mapperXmlConfig, context, modulePackage, IS_COVER_GENERATOR);//覆盖生成(此处是覆盖生成 <resultMap id="BaseResultMap" 标签中的内容)
					}else{
						//存在此文件就进行替换
						xmlStr = IOUtil.toString(new FileInputStream(file), "utf-8");
						indexS = xmlStr.indexOf(">",xmlStr.indexOf("<resultMap"))+1;
						String beforeXml = xmlStr.substring(0,indexS);
						String afterXml = xmlStr.substring(xmlStr.indexOf("</resultMap>",indexS));
						String completeXml = beforeXml+"\r\n"+resultMapElements+"\r\n"+afterXml;
						IOUtil.copy(new ByteArrayInputStream(completeXml.getBytes("UTF-8")), new FileOutputStream(file));
					}
					context.remove("resultMapElements");
				} catch (Exception e) {
					log.error("生成 {} 异常！",mapperXmlConfig.getFileName(),e);
				}
				xmlFile.delete();
			}
			
			log.info("========生成表 {} 的mapper xml映射文件 结束========\r\n",tableName);

			
			
			log.info("========生成表 {} 的mapper dao 开始========",tableName);
			createFileByTemplate(VM.get("mapper"), context, modulePackage, IS_COVER_GENERATOR);
			log.info("========生成表 {} 的mapper dao 结束========\r\n",tableName);

			
			//生成service及service impl
			if(GENERATE_SERVICE){
				log.info("========生成表 {} 的service开始========",tableName);
				createFileByTemplate(VM.get("service"), context, modulePackage, IS_COVER_GENERATOR);
				log.info("========生成表 {} 的service结束========\r\n",tableName);
				
				log.info("========生成表 {} 的service impl开始========",tableName);
				createFileByTemplate(VM.get("serviceImpl"), context, modulePackage, IS_COVER_GENERATOR);
				log.info("========生成表 {} 的service impl结束========\r\n",tableName);
			}
			//生成controller
			if(GENERATE_CONTROLLER){
				log.info("========生成表 {} 的controller开始========",tableName);
				createFileByTemplate(VM.get("controller"), context, modulePackage, IS_COVER_GENERATOR);
				log.info("========生成表 {} 的controller结束========\r\n",tableName);
			}
		}
	}
	
	
	
	/**
	 * 根据模板生成文件
	 * @param templateConfig
	 * @param context
	 * @param modulePackage
	 * @param isCover 是否覆盖
	 */
	private static void createFileByTemplate(TemplateModel templateConfig,VelocityContext context,String modulePackage, boolean isCover){
		
		String fileName = VelocityUtil.generateTemplateStrInstanceToString(context, templateConfig.getFileName());
		String dir = getBasicModulePath(modulePackage)+templateConfig.getModulePath();
		createDirs(dir);
		File file = new File(dir+fileName);
		if(isCover && file.exists()){
			file.delete();
		}
		if(!file.exists()){
			try {
				file.createNewFile();
				VelocityUtil.generateTemplateInstance(context, templateConfig.getVmPath(), new FileOutputStream(file));
			} catch (IOException e) {
				log.error("生成 {} 文件失败！",fileName,e);
			}
		}else{
			log.info("{} 已经存在,跳过！",fileName);
		}
		
	}
	
	
	/**
	 * 创建此目录
	 * @param dir
	 * @return
	 */
	private static String createDirs(String dir){
		File dirFile = new File(dir);
		if(!dirFile.exists()){
			dirFile.mkdirs();
		}
		return dir;
	}
	
	/**
	 * 根据模块包获取此模块包的路径(并创建该目录)
	 * @param modulePackage
	 * @return
	 */
	private static String getBasicModulePath(String modulePackage){
		modulePackage = BASIC_PATH + "/" + modulePackage.replace(".", "/");
		return StringUtil.addSuffix(modulePackage, "/");
	}
	
	/**
	 * 生成根据配置文件生成model
	 * @param configIn
	 */
	private static void generateByConfig(InputStream configIn){
		try {
			List<String> warnings = new ArrayList<String>();
			boolean overwrite = true;
			ConfigurationParser cp = new ConfigurationParser(warnings);
			Configuration config = cp.parseConfiguration(configIn);
			DefaultShellCallback callback = new DefaultShellCallback(overwrite);
			MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
			myBatisGenerator.generate(null);
		} catch (Exception e) {
			log.error("生成model异常！", e);
			throw new RuntimeException("生成model异常！",e);
		}finally{
			IOUtil.close(configIn);
		}
	}
	
	
	/**
	 * 根据数据连接获取数据库名称,只支持mysql
	 * @param dbUrl
	 * @return
	 */
	private static String getDbNameByUrl(String dbUrl){
		return dbUrl.substring(dbUrl.lastIndexOf("/")+1, dbUrl.lastIndexOf("?") == -1 ? dbUrl.length() :  dbUrl.lastIndexOf("?"));
	}
	
	/**
	 * 获取数据库所有表的主键及是否是自增长信息。
	 * @param dbName
	 * @throws SQLException 
	 */
	private static Map<String,TablePrimaryModel> getPrimaryKey(DruidDataSource dataSourcePool,String dbName){
		Map<String,TablePrimaryModel> result = new HashMap<String,TablePrimaryModel>();
		String sql = "SELECT "+
						"t.table_name table_name, "+
						"c.column_name column_name, "+
						"case when ts.AUTO_INCREMENT is not null then 'true' else 'false' end is_auto_increment "+
					  "FROM "+
						  "(select * from information_schema.TABLE_CONSTRAINTS where TABLE_SCHEMA = ? and CONSTRAINT_TYPE = 'PRIMARY KEY') t inner join "+
						  "(select * from information_schema.tables where TABLE_SCHEMA = ?) ts on t.TABLE_NAME = ts.TABLE_NAME inner join "+
						  "(select * from information_schema.KEY_COLUMN_USAGE where TABLE_SCHEMA = ?) c on ts.TABLE_NAME  = c.TABLE_NAME";
		List<Map<String, Object>> list = null;
		try {
			list = BasicDao.getDao().findBySql(sql, new String[]{dbName,dbName,dbName}, dataSourcePool.getConnection());
			if(EmptyUtils.isNotEmpty(list)){
				for(Map<String, Object> map : list){
					result.put(map.get("table_name").toString(), new TablePrimaryModel(map.get("table_name").toString(), map.get("column_name").toString(), Boolean.parseBoolean(map.get("is_auto_increment").toString())));
				}
			}else{
				log.warn("数据库：{},没有表设置主键！",dbName);
			}
		} catch (SQLException e) {
			log.error("查询表主键异常！", e);
			throw new RuntimeException("查询表主键异常！",e);
		}
		return result;
	}
	
	
	/**
	 * 根据表获取参数
	 * @return
	 */
	private static VelocityContext getParameter(String tableName,String modelName,String modulePackage,TablePrimaryModel tablePrimaryModel,DruidDataSource dataSourcePool){
		VelocityContext context = new VelocityContext();
		context.put("modulePackage", modulePackage);//模块包名
		context.put("modelName", modelName);//model名称
		context.put("modelNameLowerCase", modelName.toLowerCase());//model名称小写
		context.put("tableName", tableName);
		context.put("jdbcDriver", dataSourcePool.getDriverClassName());
		String url = dataSourcePool.getUrl();
		int index = url.lastIndexOf("?");
		if(index > -1){
			url = url.substring(0,index);
		}
		context.put("jdbcUrl", url);
		context.put("jdbcName", dataSourcePool.getUsername());
		context.put("jdbcPassword", dataSourcePool.getPassword());
		context.put("primaryColumnName", tablePrimaryModel.getPrimaryColumnName());
		context.put("autoIncrement", tablePrimaryModel.isAutoIncrement());
		context.put("basicPath", BASIC_PATH);
		context.put("basicModelModulePackage", StringUtil.deleteSuffix(VM.get("basicModel").getModulePath().replace("/", "."), "."));
		context.put("mapperXmlModulePackage", StringUtil.deleteSuffix(VM.get("mapperxml").getModulePath().replace("/", "."), "."));
		context.put("controllerModulePackage", StringUtil.deleteSuffix(VM.get("controller").getModulePath().replace("/", "."), "."));
		context.put("serviceImplModulePackage", StringUtil.deleteSuffix(VM.get("serviceImpl").getModulePath().replace("/", "."), "."));
		context.put("serviceModulePackage", StringUtil.deleteSuffix(VM.get("service").getModulePath().replace("/", "."), "."));
		context.put("modelModulePackage", StringUtil.deleteSuffix(VM.get("model").getModulePath().replace("/", "."), "."));
		context.put("mapperModulePackage", StringUtil.deleteSuffix(VM.get("mapper").getModulePath().replace("/", "."), "."));
		return context;
	}
	
	
	
	/**
	 * 获取模块名称
	 */
	public static abstract class ModuleName{
		
		/**
		 * 根据表名获取模块名称
		 * 表名默认遵循【类型_模块_表名】命名规程
		 * 否则自定义获取模块
		 * @param tableName
		 * @return
		 */
		public String getModuleName(String tableName){
			String moduleName = "comm";
			try{
				moduleName = tableName.substring(tableName.indexOf("_")+1, tableName.indexOf("_", tableName.indexOf("_")+1));
			}catch(Exception e){
				log.warn("根据表名获取模块名称异常！表名：'{}',将使用默认的模块'comm'",tableName);
			}
			return moduleName;
		}
		
	}
}
